package shaka.core.internal

import java.net.URL
import java.net.URLDecoder
import java.util.jar.JarEntry
import java.util.jar.JarFile

/**
 * 取得 package 中的 resource (可讀取 file 或是 jar)
 * 
 * @author Shaka
 *
 */
public class PkgFileReaderImpl implements Serializable {
	// 讀取檔案編碼
	private String encoding
	// 快取控制物件
	private Map cache = [:] as HashMap
	// 預設文字讀取檔案
	private Closure defaultCallback = { InputStream _in ->
		StringBuffer sb = new StringBuffer()
		BufferedReader reader = new BufferedReader(new InputStreamReader(_in, encoding))
		try{
			for (String line = reader.readLine(); line != null;) {
				sb.append(line)
				line = reader.readLine()
				if (line != null)
					sb.append('\n')
			}
			return sb.toString()
		}finally{
			reader.close()
		}
	}

	public PkgFileReaderImpl(String encoding) {
		this.encoding = encoding
	}

	/**
	 * 清除快取
	 */
	public void clearCache() {
		synchronized(cache) {
			cache.clear()
		}
	}

	/**
	 * 取得 class loader
	 * @return
	 */
	private ClassLoader getCL(){
		return Thread.currentThread().getContextClassLoader()
	}

	/**
	 * 讀取檔案
	 * @param packageName package 路徑
	 * @param fileName 檔案名稱
	 * @param callback 檔案讀取方式(預設為 文字讀取, 並回傳 String), 傳入一個 InputStream, 在 closure 中一定要關閉串流
	 * @return 回傳格式依 callback 決定回傳的物件
	 */
	public Object readPackageFile(String packageName, String fileName, Closure callback) {
		String readName = packageName.replace('.', '/') + '/' + fileName
		if(!cache.containsKey(readName)) {
			synchronized(cache) {
				cache[readName] = _readPackageFile(readName, callback)
			}
		}
		return cache[readName]
	}

	/**
	 * 讀取檔案實作
	 * @param name
	 * @param callback
	 * @return
	 */
	private Object _readPackageFile(String name, Closure callback) {
		InputStream _in = getCL().getResourceAsStream(name)
		if (_in == null)
			throw new FileNotFoundException(name)

		return callback?callback(_in):defaultCallback(_in)
	}

	/**
	 * 取得 package 中的 Class (包含子 package)
	 * @param packageName package 名稱
	 * @param throwErrorIfFornameError 碰到問題時是否要拋出 error
	 * @param errorHandleClosure 發生問題時的 handler (傳入 Exception, className)
	 * @return Class集合
	 */
	public Collection findClasses(String packageName,boolean throwErrorIfFornameError,Closure errorHandleClosure){
		String readName = packageName.replace('.', '/')
		return _findClasses(readName, []as ArrayList, throwErrorIfFornameError, errorHandleClosure)
	}

	/**
	 * 取得 package 中的 properties (包含子 package)
	 * @param packageName
	 * @return
	 */
	public Collection findProperties(String packageName){
		String readName = packageName.replace('.', '/')
		return _findProperties(readName,[])
	}

	private Collection _findProperties(String pkg,Collection c){
		ClassLoader classLoader = getCL()
		URL packageURL = classLoader.getResource(pkg)
		if(packageURL == null)
			throw new Exception("unknow package [${pkg}]")
		String protocol = packageURL.getProtocol()
		switch(protocol){
			case 'jar':
				String jarFileName = URLDecoder.decode(packageURL.getFile(), encoding)
				jarFileName = jarFileName.substring(5, jarFileName.indexOf("!"))
				JarFile jf = new JarFile(jarFileName)
				return _findClassesJar(jf, c, '.properties'){ String entryName ->
					c << entryName
				}
			case 'file':
				return _findClassesFile(packageURL, pkg, c, '.properties'){ File list ->
					String path = list.getAbsolutePath()
					path = path.replaceAll('\\\\', '/')
					path = path.substring(path.indexOf(pkg))
					c << path
				}
			default:
				throw new Exception("Unsupport $pkg protocol $protocol")
		}
	}

	/**
	 * 取得 package 中的 Class (包含子 package)
	 * @param pkg
	 * @param c
	 * @param throwErrorIfFornameError
	 * @param errorHandleClosure
	 * @return
	 */
	private Collection _findClasses(String pkg,Collection c,boolean throwErrorIfFornameError,Closure errorHandleClosure){
		ClassLoader classLoader = getCL()
		URL packageURL = classLoader.getResource(pkg)
		String protocol = packageURL.getProtocol()
		switch(protocol){
			case 'jar':
				String jarFileName = URLDecoder.decode(packageURL.getFile(), encoding)
				jarFileName = jarFileName.substring(5, jarFileName.indexOf("!"))
				JarFile jf = new JarFile(jarFileName)
				return _findClassesJar(jf, c, '.class'){ String entryName ->
					//去掉副檔名
					entryName = entryName.substring(0, entryName.lastIndexOf('.'))
					//轉成class name
					String className = entryName.replaceAll('/', '.')
					try{
						c.add(Class.forName(className))
					}catch(Throwable e){
						errorHandleClosure(e,className)
						if(throwErrorIfFornameError)throw e
					}
				}
			case 'file':
				return _findClassesFile(packageURL, pkg, c, '.class'){ File list ->
					//去掉副檔名
					String path = list.getAbsolutePath()
					path = path.replaceAll('\\\\', '/')
					path = path.substring(path.indexOf(pkg), path.lastIndexOf('.'))
					//轉成class name
					String className = path.replaceAll('/', '.')
					try{
						c.add(Class.forName(className))
					}catch(Throwable e){
						errorHandleClosure(e,className)
						if(throwErrorIfFornameError)throw e
					}
				}
			default:
				throw new Exception("Unsupport $pkg protocol $protocol")
		}
	}

	/**
	 * 找出 package(jar)(含子package)的name
	 * @param jf
	 * @param c
	 * @param extName 副檔名
	 * @param handleClosure 傳入 jar entry name
	 * @return
	 */
	private Collection _findClassesJar(JarFile jf,Collection c,String extName,Closure handleClosure){
		for(JarEntry jarEntry : jf.entries()){
			String entryName = jarEntry.getName()
			if (!jarEntry.isDirectory() && entryName.endsWith(extName) && entryName.indexOf('$') == -1){
				handleClosure(entryName)
			}
		}
		return c
	}

	/**
	 * 找出 package(jar)(含子package)的name
	 * @param packageURL
	 * @param packageName
	 * @param c
	 * @param extName 副檔名
	 * @param handleClosure 傳入 File
	 * @return
	 */
	private Collection _findClassesFile(URL packageURL,String packageName,Collection c,String extName,Closure handleClosure){
		String fn = URLDecoder.decode(packageURL.getFile(), encoding)
		File folder = new File(fn)
		LinkedList folders = new LinkedList()
		folders.add(folder)
		while(folders.size() != 0){
			File f = folders.removeLast()
			if(!f.isDirectory())
				continue
			for(File list : f.listFiles()){
				String entryName = list.getName()
				if(list.isDirectory()){
					folders.add(list)
				}else if(entryName.endsWith(extName) && entryName.indexOf('$') == -1){
					handleClosure(list)
				}
			}
		}
		return c
	}
}
